using System;
using System.Collections.Generic;
using System.Linq;
using Headsight.BPMN2WF.BPMN.Schemas;
using Headsight.BPMN2WF.BPMN.Utils;

namespace Headsight.BPMN2WF.BPMN.Model
{
    /// <summary>
    /// <i>BPMN2WF</i> <see cref="tProcess"/> representation.
    /// </summary>
    public class BPMNFlowNodesBlock : BPMNFlowNode
    {
        private readonly IDictionary<BPMNFlowNode, IList<BPMNFlowNode>> _bpmnFlowNodes;
        private IDictionary<String, BPMNSequenceFlow> _bpmnSequenceFlows;
        private readonly BPMNFlowNode _startProcessFlowNode;

        public BPMNFlowNodesBlock(BPMNFlowNode startNode) :
            this(startNode,
            new List<BPMNArgument>(),
            new Dictionary<BPMNFlowNode, IList<BPMNFlowNode>>(),
            new Dictionary<String, BPMNSequenceFlow>())
        {
            _bpmnFlowNodes[startNode] = new List<BPMNFlowNode>();
        }

        /// <summary>
        /// Constructs a new <see cref="BPMNFlowNodesBlock"/> instance  wrapping <para>tProcess</para>.
        /// </summary>
        public BPMNFlowNodesBlock(BPMNFlowNode startNode, IList<BPMNArgument> arguments, IDictionary<BPMNFlowNode, IList<BPMNFlowNode>> bpmnFlowNodes, IDictionary<String, BPMNSequenceFlow> bpmnSequenceFlows)
            : base(startNode.TElement, new List<BPMNArgument>(), new List<BPMNArgument>())
        {
            Id = String.Format("block_{0}", startNode.Id);
            _startProcessFlowNode = startNode;

            Arguments = arguments;
            _bpmnFlowNodes = bpmnFlowNodes;
            _bpmnSequenceFlows = bpmnSequenceFlows;
        }

        /// <summary>
        /// <i>Full qualified type name</i> as needed / to be set for WF workflows.
        /// </summary>
        public String FullQualifiedName { get; set; }

        public BPMNFlowNode AddFlow(BPMNFlowNode start, BPMNFlowNode end)
        {
            IList<BPMNFlowNode> elements;

            if (_bpmnFlowNodes.ContainsKey(start))
            {
                elements = _bpmnFlowNodes[start];
            }
            else
            {
                _bpmnFlowNodes[start] = elements = new List<BPMNFlowNode>();
            }

            if (!_bpmnFlowNodes.ContainsKey(end))
            {
                _bpmnFlowNodes[end] = new List<BPMNFlowNode>();
            }

            if (!elements.Contains(end))
            {
                elements.Add(end);
            }

            /*
            if (!_bpmnSequenceFlows.ContainsKey(sequence.Id))
            {
                _bpmnSequenceFlows[sequence.Id] = sequence;
            }
            */
            return end;
        }

        public BPMNFlowNode AddFlowFrom2(BPMNFlowNode parent, BPMNFlowNode next)
        {
            IList<BPMNFlowNode> elements;

            if (_bpmnFlowNodes.ContainsKey(parent))
            {
                elements = _bpmnFlowNodes[parent];
            }
            else
            {
                _bpmnFlowNodes[parent] = elements = new List<BPMNFlowNode>();
            }

            if (!_bpmnFlowNodes.ContainsKey(next))
            {
                _bpmnFlowNodes[next] = new List<BPMNFlowNode>();
            }

            if (!elements.Contains(next))
            {
                elements.Add(next);
            }

            return next;
        }

        public IList<BPMNFlowNode> GetNextNodesInFlowFrom(BPMNFlowNode element)
        {
            return _bpmnFlowNodes[element];
        }

        public BPMNFlowNode NodeOut { get; set; }

        public virtual IList<BPMNFlowNode> AllNodes
        {
            get
            {
                return _bpmnFlowNodes.Keys.ToList();
            }
        }

        public virtual IList<BPMNFlowNode> AllNodesWithoutNodeOut
        {
            get
            {
                var result = new List<BPMNFlowNode> {FlowNodeIn};

                foreach (var each in AllNodes)
                {
                    if (each == FlowNodeIn || each == NodeOut)
                    {
                        continue;
                    }

                    if (each is BPMNFlowNodesBlock)
                    {
                        result.AddRange(((BPMNFlowNodesBlock)each).AllNodesWithoutNodeOut);
                        if (((BPMNFlowNodesBlock)each).NodeOut != null)
                        {
                            result.Add(((BPMNFlowNodesBlock)each).NodeOut);
                        }
                    }
                    else
                    {
                        result.Add(each);
                    }
                }

                return result;
            }
        }

        /// <summary>
        /// Returns the <see cref="tProcess"/>'s <i>start process <see cref="tFlowElement"/></i>.
        /// </summary>
        public BPMNFlowNode FlowNodeIn
        {
            get
            {
                return _startProcessFlowNode ??
                    // Backward compatiblity 
                    BPMNFlowNodes.FirstOrDefault(x => x.TElement is tStartEvent)
                    ??
                    // backward compability 
                    BPMNFlowNodes.FirstOrDefault(x => GetNumberOfIncomingFlows(x) == 0);
            }
        }

        /// <summary>
        /// Returns <see cref="tProcess"/>'s <see cref="tFlowNode"/>s as instances of <see cref="BPMNFlowNode"/>.
        /// </summary>
        public IList<BPMNFlowNode> BPMNFlowNodes { get { return _bpmnFlowNodes.Keys.ToList(); } }

        /// <summary>
        /// Returns <see cref="tProcess"/>'s <see cref="tSequenceFlow"/>s as instances of <see cref="BPMNSequenceFlow"/>.
        /// </summary>
        public IList<BPMNSequenceFlow> BPMNSequenceFlows { get { return _bpmnSequenceFlows.Values.ToList(); } }

        /// <summary>
        /// Returns the <see cref="tProcess"/>'s  <see cref="tFlowNode"/> having the <see cref="tFlowNode.id"/> equal
        /// to <para>id</para> wrapped into an instance of <see cref="BPMNFlowNode"/>.
        /// </summary>
        public BPMNFlowNode GetBPMNFlowNodeById(String id)
        {
            try
            {
                var result = AllNodes.FirstOrDefault(x => x.Id == id);

                if (result != null)
                {
                    return result;
                }

                return AllNodes.OfType<BPMNFlowNodesBlock>()
                    .Select(each => each.GetBPMNFlowNodeById(id))
                    .FirstOrDefault(y => y != null);
            }
            catch (Exception e)
            {
                throw new Exception(String.Format("Id: {0}", id), e);
            }
        }

        /// <summary>
        /// Returns the <see cref="tProcess"/>'s  <see cref="tSequenceFlow"/> having the <see cref="tSequenceFlow.id"/> equal
        /// to <para>id</para> wrapped into an instance of <see cref="BPMNSequenceFlow"/>.
        /// </summary>

        public BPMNSequenceFlow GetBPMNSequenceFlowById(String id)
        {
            return _bpmnSequenceFlows[id];
        }

        /// <summary>
        /// Returns all <see cref="BPMNFlowNode"/>s having any incoming <see cref="BPMNSequenceFlow"/>
        /// of the passed in <see cref="BPMNFlowNode"/>.
        /// <br/>
        /// Eventually it would be nicer to have this method on <see cref="BPMNFlowNode"/> but this works for now and is 
        /// much easier. Personally i desire to keep the object graph quite flat as long as possible.
        /// </summary>
        public IList<BPMNFlowNode> GetTargetBPMNFlowNodes(BPMNFlowNode bpmnFlowNode)
        {
            return GetTargetBPMNFlowNodes(bpmnFlowNode.Id);
        }

        /// <summary>
        /// Returns all <see cref="BPMNFlowNode"/>s having any incoming <see cref="BPMNSequenceFlow"/>
        /// of the passed in <see cref="BPMNFlowNode"/>.
        /// <br/>
        /// Eventually it would be nicer to have this method on <see cref="BPMNFlowNode"/> but this works for now and is 
        /// much easier. Personally i desire to keep the object graph quite flat as long as possible.
        /// </summary>
        public IList<BPMNFlowNode> GetTargetBPMNFlowNodes(String id)
        {
            try
            {
                return _bpmnFlowNodes.First(x => x.Key.Id == id).Value;
            }
            catch (Exception e)
            {
                throw new Exception(String.Format("Id: {0}", id), e);
            }
        }

        /// <summary>
        /// <i>The arguments</i>.
        /// </summary>
        public IList<BPMNArgument> Arguments { get; private set; }

        public Int32 NumBPMNFlowNodes
        {
            get { return _bpmnFlowNodes.Count(); }
        }

        public IList<BPMNSequenceFlow> BPMNSequenceFlowsTest
        {
            set { value.ForEach(x => _bpmnSequenceFlows[x.Id] = x);}
        }

        public BPMNFlowNodesBlock Reduce(BPMNFlowNode bpmnFlowNode)
        {
            var bpmnProcessBlock = bpmnFlowNode as BPMNFlowNodesBlock;

            if (bpmnProcessBlock == null)
            {
                return this;
            }

            var result = new BPMNFlowNodesBlock(FlowNodeIn);

            foreach (var each in _bpmnFlowNodes)
            {
                result._bpmnFlowNodes[each.Key] = new List<BPMNFlowNode>(each.Value);
            }

            foreach (var each in bpmnProcessBlock.AllNodesWithoutNodeOut.Where(x => x.Id != bpmnProcessBlock.FlowNodeIn.Id))
            {
                result._bpmnFlowNodes.Remove(each);
            }

            if (bpmnProcessBlock.NodeOut == null || bpmnProcessBlock.NodeOut.Id != bpmnProcessBlock.FlowNodeIn.Id)
            {
                result._bpmnFlowNodes.Remove(bpmnProcessBlock.FlowNodeIn);
            }

            result._bpmnFlowNodes[bpmnProcessBlock] = new List<BPMNFlowNode>();

            foreach (var each in result._bpmnFlowNodes.Values)
            {
                if (each.Remove(bpmnProcessBlock.FlowNodeIn))
                {
                    each.Add(bpmnProcessBlock);
                }
            }

            if (bpmnProcessBlock.NodeOut != null)
            {
                try
                {
                    var endNodesNextVertices = result._bpmnFlowNodes[bpmnProcessBlock.NodeOut];
                    result._bpmnFlowNodes.Remove(bpmnProcessBlock.NodeOut);
                    result._bpmnFlowNodes[bpmnProcessBlock] = endNodesNextVertices;
                }
                catch (Exception e)
                {
                    throw new Exception(String.Format("Id: {0}, FlowNodeIn: {1}, FlowNodeOut: {2}, Key: {3}", bpmnProcessBlock, bpmnProcessBlock.FlowNodeIn, bpmnProcessBlock.NodeOut, bpmnProcessBlock.NodeOut), e);
                }
            }

            // result._bpmnFlowNodes = _bpmnFlowNodes;
            result._bpmnSequenceFlows = _bpmnSequenceFlows;

            return result;
        }

        public bool IsAncestorOf(BPMNFlowNode supposedBPMNFlowNodeParent, BPMNFlowNode supposedBPMNFlowNodeChild)
        {
            var nextBpmnFlowNodes = GetTargetBPMNFlowNodes(supposedBPMNFlowNodeParent);

            if (nextBpmnFlowNodes.Contains(supposedBPMNFlowNodeChild))
            {
                return true;
            }

            return nextBpmnFlowNodes.Any(x => IsAncestorOf(x, supposedBPMNFlowNodeChild));
        }

        public int GetNumberOfIncomingFlows(BPMNFlowNode nodeId)
        {
            return _bpmnFlowNodes.Values.Sum(each => each.Count(x => x.Id == nodeId.Id));
        }

        /// <summary>
        /// Returns the next <see cref="BPMNFlowNode"/> into which <i>all</i> outgoing 
        /// flows of <para>flowNode</para> are converging again or <code>null</code>
        /// if no such <see cref="BPMNFlowNode"/> exists.
        /// </summary>
        public BPMNFlowNode GetConvergingFlowNode(BPMNFlowNode flowNode)
        {
            BPMNFlowNode result = null;

            //var flowNodesNumOutgoingFlows = GetNumberOfOutgoingFlows(flowNode);
            var flowNodesNext = GetTargetBPMNFlowNodes(flowNode);
            var flowNodes = GetFlowNodesFrom(flowNode).Where(x => GetNumberOfIncomingFlows(x) > 1
                );//&& GetNumberOfIncomingFlows(x) <= flowNodesNumOutgoingFlows);

            //var flowNodes2 = GetFlowNodesFrom(flowNode);

            // all flowNodes reachable via flowNode must be ancestor of 
            // possible converging flow node. Flow nodes not fullfilling this condition
            // are flow nodes branching somewhere into branches not actually
            // converging .... 
            //            flowNodes = flowNodes.Where(x => flowNodes2.All(y => IsAncestorOf(y, x)));

            foreach (var each in flowNodes)
            {
                var eachNumAncestors = GetNumAncestorsOf(flowNodesNext, each);

                if (eachNumAncestors > 1)
                {
                    var resultNumAncestors = GetNumAncestorsOf(flowNodesNext, result);

                    if (eachNumAncestors > resultNumAncestors)
                    {
                        result = each;
                    }
                }
            }

            return result;
        }

        private Int32 GetNumAncestorsOf(IEnumerable<BPMNFlowNode> flowNodesAncestors, BPMNFlowNode flowNode)
        {
            return flowNodesAncestors.Count(x => IsAncestorOf(x, flowNode) || x == flowNode);
        }

        private IEnumerable<BPMNFlowNode> GetFlowNodesFrom(BPMNFlowNode flowNode)
        {
            var result = new List<BPMNFlowNode>();

            foreach (var each in BPMNFlowNodes)
            {
                if (IsAncestorOf(flowNode, each) && !result.Contains(each))
                {
                    result.Add(each);
                }
            }

            return result;
        }

        /// <summary>
        /// Returns the number of <see cref="BPMNFlowNode"/>s following the <see cref="BPMNFlowNode"/>
        /// <para>bpmnFlowNode</para>.
        /// </summary>
        public Int32 GetNumberOfOutgoingFlows(BPMNFlowNode bpmnFlowNode)
        {
            return GetTargetBPMNFlowNodes(bpmnFlowNode).Count;
        }

        public BPMNFlowNodesBlock GetBPMNSubProcess(BPMNFlowNode flowStart, BPMNFlowNode flowEnd)
        {
            var subProcessNodes = new List<BPMNFlowNode> { flowStart };
            subProcessNodes.AddRange(BPMNFlowNodes.Where(x => IsAncestorOf(flowStart, x)
                && x != flowEnd));

            var dicFlowNodes = new Dictionary<BPMNFlowNode, IList<BPMNFlowNode>>();
            var dicSeqFlows = _bpmnSequenceFlows;// new Dictionary<String, BPMNSequenceFlow>();

            foreach (var each in subProcessNodes)
            {
                dicFlowNodes[each] = GetTargetBPMNFlowNodes(each).Where(subProcessNodes.Contains).ToList();
            }

            return new BPMNFlowNodesBlock(flowStart, Arguments, dicFlowNodes, dicSeqFlows);
        }

        public BPMNFlowNode GetPreviousFlowNode(BPMNFlowNode flowNodeStart, BPMNFlowNode flowNode)
        {
            if (GetTargetBPMNFlowNodes(flowNodeStart).Contains(flowNode))
            {
                return flowNodeStart;
            }

            return BPMNFlowNodes.Where(x => IsAncestorOf(flowNodeStart, x) && IsAncestorOf(x, flowNode))
                .FirstOrDefault(x => GetTargetBPMNFlowNodes(x).Contains(flowNode));
        }

        public BPMNFlowNodesBlock GetBPMNSubProcess2(IList<BPMNFlowNode> flowNodes, BPMNFlowNode flowStart, BPMNFlowNode flowEnd)
        {
            var abc = new List<BPMNFlowNode>();
            abc.AddRange(flowNodes);
            abc.Remove(flowStart);

            var subProcessNodes = new List<BPMNFlowNode> { flowStart };
            subProcessNodes.AddRange(BPMNFlowNodes.Where(x => IsAncestorOf(flowStart, x) && x != flowEnd
                && !abc.Any(y => IsAncestorOf(y, x))));

            return new BPMNFlowNodesBlock(FlowNodeIn, Arguments,
                null, null);
        }

        public BPMNSequenceFlow GetBPMNFlow(BPMNFlowNode startFlowNode, BPMNFlowNode bpmnFlowNode)
        {
            return
                _bpmnSequenceFlows.Values.FirstOrDefault(
                    x => x.TElement.sourceRef == startFlowNode.Id && x.TElement.targetRef == bpmnFlowNode.Id);
        }
    }
}